<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
    <style>
        s {
            color: indianred;
            text-decoration: none;
        }
    </style>
</head>
<body>
<div id="app">
    <h1><s>{{song}}</s></h1>
    <p>《<s>{{album.name}}</s>》是 <s>{{singer}}</s> 2005年11月发行的专辑</p>
    <p>主打歌为《<s>{{album.theme}}</s>》</p>
    <p>作词人为 <s>{{singer}}</s> 等人。</p>
    为你弹奏肖邦的<s>《{{album.theme}}》</s>
    <p>还有以下歌曲：<s>{{album.data.arr}}</s></p>
    <p>求值：<s>{{sum}}</s></p>
    <p>求值2：<s>{{sum2}}</s></p>
</div>
<script>
    const isObject = function (obj) {
        return Object.prototype.toString.call(obj) === '[object Object]';
    };

    const isFunction = function (obj) {
        return typeof obj === 'function';
    };

    class Mvvm {
        constructor (options = {}) {
            this.$options = options;
            // this.$methods = options.methods;
            Object.assign(this, options.methods);
            this.$dep = new Dep();
            this.$computeDep = new ComputeDep();
            this._data = this.observe({
                ...this.$options.computed,
                ...options.data
            });
            if (isFunction(options.beforeMounted)) {
                options.beforeMounted.apply(this);
            }
            this.compile(options.el, this);
            if (isFunction(options.mounted)) {
                options.mounted.apply(this);
            }
        }

        /**
         * 获取watcher的Key
         */
        getKey (parentKey, key) {
            return parentKey ? (parentKey + '.' + key) : key;
        }

        createProxy (data, parentKey) {
            const that = this;
            const specialArr = ['toString', 'join'];
            return new Proxy(data, {
                get: function (target, key) {
                    const originData = target[key];
                    if (isFunction(originData) && !specialArr.includes(key)) {
                        return originData.call(that.data);
                    }
                    return originData;
                },
                set: function (target, key, val) {
                    if (target[key] === val) {
                        return true;//对于数组，需要返回true，否则使用反射会失败
                    }
                    const res = Reflect.set(target, key, val);
                    if (Array.isArray(target)) {
                        that.$dep.notify(that.getKey('', parentKey));
                        that.$computeDep.notify(that.getKey('', parentKey));
                    } else {
                        that.$dep.notify(that.getKey(parentKey, key));
                        that.$computeDep.notify(that.getKey(parentKey, key));
                    }
                    return res;
                }
            });
        }

        observe (data, parentKey) {
            const proxy = this.createProxy(data, parentKey);
            for (let key in data) {
                if (isObject(data[key]) || Array.isArray(data[key])) {
                    Reflect.set(proxy, key, this.observe(data[key], this.getKey(parentKey, key)));
                }
            }
            return proxy;
        }

        get data () {
            return this._data;
        }

        compile (el, vm) {
            const that = this;
            vm.$el = document.querySelector(el);
            // 在el范围里将内容都拿到，当然不能一个一个的拿
            // 可以选择移到内存中去然后放入文档碎片中，节省开销
            let fragment = document.createDocumentFragment();

            let child;
            while (child = vm.$el.firstChild) {
                fragment.appendChild(child);    // 此时将el中的内容放入内存中
            }

            // 对el里面的内容进行替换
            function replace (frag) {
                Array.from(frag.childNodes).forEach(node => {
                    let txt = node.textContent;
                    let reg = /\{\{(.*?)\}\}/g;   // 正则匹配{{}}

                    if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
                        // console.log(RegExp.$1); // 匹配到的第一个分组 如：a.b, c

                        function replaceTxt () {
                            node.textContent = txt.replace(reg, (matched, placeholder) => {
                                console.log(placeholder, node.data);   // 匹配到的分组 如：song, album.name, singer...
                                const isComputed = placeholder in that.$options.computed;
                                const dep = isComputed ? that.$computeDep : that.$dep;
                                if (!dep.hasSub(node, placeholder)) {
                                    const watcher = new Watcher(vm, placeholder, node, isComputed, replaceTxt);   // 监听变化，进行匹配替换内容
                                    dep.add(watcher, isComputed);
                                }

                                return placeholder.split('.').reduce((val, key) => {
                                    return val[key];
                                }, vm.data);
                            });
                        }

                        // 替换
                        replaceTxt();
                    }
                    // 如果还有子节点，继续递归replace
                    if (node.childNodes && node.childNodes.length) {
                        replace(node);
                    }
                });
            }

            replace(fragment);  // 替换内容

            vm.$el.appendChild(fragment);   // 再将文档碎片放入el中
        }
    }

    class Dep {
        constructor () {
            this.subs = {};
        }

        hasSub (node, exp) {
            if (!this.subs[exp]) {
                return false;
            }
            return this.subs[exp].some((item) => {
                if (item.node === node) {
                    return true;
                }
            });
        }

        add (sub) {
            const {exp} = sub;
            if (!this.subs[exp]) {
                this.subs[exp] = [];
            }
            this.subs[exp].push(sub)
        }

        notify (exp) {
            if (this.subs[exp]) {
                this.subs[exp].forEach((sub) => sub.update());
            }
        }
    }

    class ComputeDep extends Dep {
        notify (exp) {
            for (let exp in this.subs) {
                this.subs[exp].forEach((sub) => sub.update());
            }
        }
    }

    class Watcher {
        constructor (vm, exp, node, isComputed, func) {
            this.func = func;
            this.vm = vm;
            this.exp = exp;
            this.node = node;
            this.isComputed = isComputed;
            this.lastVal = this.getVal();
        }

        getVal () {
            let arr = this.exp.split('.');
            let val = this.vm.data;
            arr.forEach(key => {
                if (val) {
                    val = val[key];   // 通过get获取到新的值
                } else {
                    val = null;
                }
            });
            return val;
        }

        update () {
            // notify的时候值已经更改了
            // 再通过vm, exp来获取新的值
            const val = this.getVal();
            if (this.isComputed && this.lastVal === val) {
                return;
            }
            this.lastVal = val;
            this.func(val);
        }
    }

    class Vue {
        constructor (options) {
            const mvvm = new Mvvm(options);
            const proxy = new Proxy(mvvm, {
                get: function (target, key) {
                    if (Reflect.has(target, key)) {
                        return Reflect.get(target, key);
                    }
                    if (key === 'data') {
                        return Reflect.get(target, key);
                    }
                    return Reflect.get(target.data, key);
                },
                set: function (target, key, value) {
                    if (key === 'data') {
                        throw new Error('Cannot set data');
                    }
                    return Reflect.set(target.data, key, value);
                }
            });
            // Object.assign(this, proxy);
            return proxy;
        }
    }
</script>
<script>
    // 写法和Vue一样
    let app = new Vue({
        // let app = new Mvvm({
        el: '#app',
        data: {     // Object.defineProperty(obj, 'song', '发如雪');
            song: '发如雪',
            album: {
                name: '十一月的肖邦',
                theme: '夜曲',
                data: {
                    arr: ['东风破', '七里香']
                }
            },
            singer: '周杰伦',
            a: 1,
            b: 2,
            c: 3
            // sum:'abc' //它的值优于computed
        },
        beforeMounted () {
            console.log('beforeMounted');
        },
        mounted () {
            setTimeout(() => {
                console.log('mounted');
            }, 1000);
        },
        methods: {
            add () {
                console.log(this);
            }
        },
        computed: {
            sum () {
                return this.a + this.b;
            },
            sum2 () {
                return this.a + this.c;
            }
        },
    });
</script>
</body>
</html>